Skip to content

fix(bump): skip commit step when there are no files to commit#1969

Open
bearomorphism wants to merge 3 commits intocommitizen-tools:masterfrom
bearomorphism:fix/1530-skip-commit-when-clean
Open

fix(bump): skip commit step when there are no files to commit#1969
bearomorphism wants to merge 3 commits intocommitizen-tools:masterfrom
bearomorphism:fix/1530-skip-commit-when-clean

Conversation

@bearomorphism
Copy link
Copy Markdown
Collaborator

@bearomorphism bearomorphism commented May 9, 2026

Description

Closes #1530.

Why

cz bump unconditionally calls git commit -a after updating version files, then treats any non-zero exit code as a fatal BumpCommitFailedError. When version_provider = "scm" is configured with no version_files and no changelog generation, there are no files for the bump to modify — so git exits with "nothing to commit, working tree clean" and the error is raised before any tag is ever created.

Reported by @loelkes on commitizen 4.8.2 / Python 3.13 / macOS (#1530): the exact error is 2nd git.commit error: "On branch main\nnothing to commit, working tree clean\n". Maintainer @Lee-W confirmed the report ("this is a valid bug"), and @woile linked a prior fix attempt (PR #996) that was never merged. A triage note from the open-issues audit (2026-05-09) found the bug is worse on master (v4.15.1) than the original report described: even the first bump now fails, because newer commitizen versions no longer modify cz.toml during a bump.

The version_provider = "scm" workflow is fully documented — the version is derived entirely from git tags and no file is ever written — yet there was no way to use it without also enabling update_changelog_on_bump as a workaround.

What changed

File Change
commitizen/git.py Add has_pending_changes() helper after is_staging_clean (line 311): runs git status --porcelain --untracked-files=no and returns True when there are tracked-file changes that git commit -a would commit
commitizen/commands/bump.py Gate the git.commit + retry block (lines 383–399) behind if git.has_pending_changes(): …; emit an out.info message and skip straight to tag creation when the tree is clean
tests/commands/test_bump_command.py Add test_bump_skips_commit_when_no_files_changed: two consecutive cz bump --yes calls in a version_provider = "scm" repo both succeed and produce the expected v0.1.0 / v0.1.1 tags

How it works

  • git.has_pending_changes() (new function, commitizen/git.py after line 311) runs git status --porcelain --untracked-files=no. The --untracked-files=no flag restricts the query to tracked files — exactly the set that git commit -a would stage and commit. The function returns True if there is any non-empty output, False if the working tree is clean for tracked files.
  • Why not reuse the existing git.is_staging_clean() (commitizen/git.py:308–311)? is_staging_clean runs git diff --cached --name-only, which checks only the index (already-staged changes). git commit -a also commits unstaged modifications to tracked files — for example a file reformatted by a pre-commit hook that wasn't re-staged. git status --porcelain --untracked-files=no covers both cases in a single call, making has_pending_changes the correct guard for git commit -a.
  • Why not treat a non-zero exit from git commit as non-fatal? The existing error path also surfaces genuine commit failures — bad GPG key, locked .git/index, pre-commit hook rejection — as non-zero exit codes. Silently swallowing those would mask real problems. Checking before attempting the commit avoids the ambiguity entirely.
  • The retry path is preserved: the pre-commit-reformatter retry (self.retry and c.return_code != 0 and self.changelog_flag) is moved inside the else branch and continues to work exactly as before — it only runs when there was something to commit and the first attempt failed.
  • When skipping the commit, out.info("No file changes; skipping bump commit and tagging HEAD.") is emitted so the user can see why no bump commit appears in git log.

Backward compatibility

  • Repos that have version_files or update_changelog_on_bump = true always have pending changes after the update step; has_pending_changes() returns True and the commit path runs exactly as before.
  • The retry-on-reformat path (self.retry) is preserved intact inside the else branch.
  • Pre-commit hook integration, GPG signing (gpg_sign), and annotated-tag options are unaffected — those code paths all execute after the commit block.
  • git.is_staging_clean() is untouched; nothing that calls it is changed.
  • All pre-existing tests/commands/test_bump_command.py tests pass (132 tests; 4 GPG-fixture tests deselected on Windows — pre-existing, unrelated).

Checklist

Was generative AI tooling used to co-author this PR?

  • Yes (please specify the tool below)

Generated-by: Claude following the guidelines

Code Changes

  • Add test cases to all the changes you introduce
  • Run uv run poe all locally to ensure this change passes linter check and tests
  • Manually test the changes (see "Steps to Test" below)
  • Update the documentation for the changes

Expected Behavior

Scenario Outcome
version_provider = "scm", no version_files, no changelog — first cz bump Tag v0.1.0 created on HEAD; no bump commit; exit 0
Same config — second cz bump after another conventional commit Tag v0.1.1 created; no error; exit 0
version_provider = "pep621" with version_files configured — cz bump Behaviour unchanged: version file updated → bump commit created → tag applied
version_provider = "scm" with update_changelog_on_bump = truecz bump Behaviour unchanged: changelog written → has_pending_changes() returns True → bump commit created → tag applied

Steps to Test This Pull Request

git fetch fork fix/1530-skip-commit-when-clean
git checkout fork/fix/1530-skip-commit-when-clean

# 1. Targeted regression test.
uv run pytest tests/commands/test_bump_command.py::test_bump_skips_commit_when_no_files_changed -v

# 2. Reproduce the bug, then verify the fix.
mkdir /tmp/cz-1530 && cd /tmp/cz-1530
git init -b main
git config user.name test && git config user.email test@example.com
printf '[tool.commitizen]\nversion_provider = "scm"\ntag_format = "v$version"\n' > cz.toml
git add cz.toml && git commit -m "feat: setup"

# First bump — previously failed with BumpCommitFailedError on master.
cz bump --yes
# Expected: "No file changes; skipping bump commit and tagging HEAD." + exit 0
git tag --list   # should show v0.1.0

# Second bump — the scenario from the original report.
touch foobar && git add foobar && git commit -m "feat: foobar"
cz bump --yes
# Expected: exit 0, no "nothing to commit" error
git tag --list   # should show v0.1.0 and v0.2.0

Additional Context

This fix was identified during the open-issues audit tracked in #1964. A triage note (@bearomorphism, 2026-05-09) confirmed the bug reproduces on master (v4.15.1) — and is worse than the original report, failing on the very first bump — and suggested checking is_staging_clean() before the commit. The implementation instead introduces a dedicated git.has_pending_changes() helper using git status --porcelain --untracked-files=no, which correctly covers both staged and unstaged modifications to tracked files (the full set that git commit -a acts on); see the "How it works" section for why the existing is_staging_clean() was insufficient. A prior fix attempt existed as PR #996 but was never merged.

When using `version_provider = "scm"` with no `version_files` and no
`--changelog` / `update_changelog_on_bump`, the bump has nothing to
commit. `cz bump` would then call `git commit -a`, which exits with
`nothing to commit, working tree clean` and the bump fails with
`BumpCommitFailedError`.

Add a `git.has_pending_changes()` helper that returns `True` only if
`git commit -a` would actually commit something, and use it to decide
whether to issue the bump commit. When there is nothing to commit, the
tag is created on `HEAD` directly. This is the typical flow for SCM-
driven projects that derive the version from the latest tag.

Closes commitizen-tools#1530

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.24%. Comparing base (4b93a50) to head (c04048c).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1969   +/-   ##
=======================================
  Coverage   98.23%   98.24%           
=======================================
  Files          61       61           
  Lines        2779     2790   +11     
=======================================
+ Hits         2730     2741   +11     
  Misses         49       49           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes cz bump failing in SCM-based versioning setups where a bump produces no file changes (no version files and no changelog), by skipping the bump commit step when there is nothing to commit and proceeding directly to tagging.

Changes:

  • Add git.has_pending_changes() to detect tracked-file changes that git commit -a would commit.
  • Gate the bump commit + retry logic behind a “pending changes” check and skip straight to tagging when clean.
  • Add a regression test covering two consecutive bumps in a version_provider = "scm" repo.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
commitizen/git.py Adds a helper to detect pending tracked changes before attempting git commit -a.
commitizen/commands/bump.py Skips the bump commit when there are no pending changes and continues to tag creation.
tests/commands/test_bump_command.py Adds regression coverage for SCM provider bumps with no file modifications.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread commitizen/commands/bump.py Outdated
Comment thread commitizen/commands/bump.py Outdated
Comment thread commitizen/git.py Outdated
bearomorphism and others added 2 commits May 9, 2026 22:30
* skip git.add when updated_files is empty

* remove now-misleading FIXME on has_pending_changes guard

* use consistent double-backtick reST formatting in has_pending_changes docstring

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The git.add return-code check added in the previous reviewer-feedback commit was not exercised by tests. This adds a regression test that mocks git.add returning non-zero and asserts BumpCommitFailedError is raised with a useful message.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2nd git.commit error: "On branch main nothing to commit, working tree clean"

2 participants